package net.chemistry;

import java.io.Serializable;
import java.util.HashMap;
import java.util.HashSet;

import net.ChemistryException;

public class Molecule implements Cloneable, Serializable
{
	private static final long serialVersionUID = 4817697985038293292L;

	private String name;
	
	private final Atom root;
	
	private boolean valid, nameset;
	
	private final HashSet<Atom> atoms = new HashSet<>();
	
	private final HashMap<AtomID,Integer> atomCount = new HashMap<>();
	
	/**
	 * Create a new molecule from atom <code>atom0</code>. 
	 * The molecule's name is initlized to the name of the atom, as specified in <code>resetName</code>.
	 * 
	 * @param atom0 An atom to initialize the molecule.
	 * @see resetName
	 * */
	public Molecule(Atom atom0)
	{
		valid = true;
		
		root = atom0;
		
		atoms.add(atom0);
		atomCount.put(atom0.atomid, 1);
		
		resetName();
	}
	
	public String toString()
	{
		return name + " @" + Integer.toHexString(hashCode());
	}
	
	public Molecule clone()
	{
		Molecule m;
		
		try
		{
			m = (Molecule) super.clone();
		}
		catch(Exception e)
		{
			return null;
		}
		
		m.valid = true;
		
		return m;
	}
	
	public boolean isIsomer(Molecule m)
	{
		return atoms.equals(m.atoms);
	}
	
	/**
	 * Returns true if the molecule is valid, and false otherwise. 
	 * <p>
	 * A molecule is considered valid if it exists on its own and is not part of a larger molecule, 
	 * e.g., a functional group. Attempting add an invalid molecule to another molecule via
	 * <code>appenMolecule</code> will throw an exception.
	 * 
	 *  @return True if the molecule is valid, false otherwise.
	 *  @see appendMolecule
	 * */
	public boolean isValid()
	{
		return valid;
	}
	
	/**
	 * Sets the molecule's name to <code>string</code>. Multiple molecules that have the same 
	 * forumula can have different names. In order to set the molecules name to that dicated by 
	 * its formula, use <code>resetName()</code>.
	 * 
	 * @see resetName
	 * @see getName
	 * */
	public void setName(String name)
	{
		this.name = name;
		nameset = true;
	}
	
	/**
	 * Sets the molecules name such that it reflects the molecule's formula. For instance, calling 
	 * <code>resetName()</code> on a water molecule would change its name to "H2O". Note that this is a
	 * strictly numerical formula; no structral formula is given.
	 * 
	 * @see setName
	 * @see getName
	 * */
	public void resetName()
	{
		name = "";
		
		for (AtomID atomid : atomCount.keySet())
		{
			name += atomid.abbrev;
			name += atomCount.get(atomid);
		}
		
		nameset = false;
	}
	
	/**
	 * Returns a fixed-size array containing all of the atoms currently contained in the molecule.
	 * Modifying the array will not reflect in the molecule's structure, although editing individual atoms will have effects on the atoms in
	 * the molecule. To perform operations on the molecule's structure, such as adding atoms, use <code>appendAtom</code> and 
	 * <code>appendGroup</code>.
	 * 
	 * @see appendAtom
	 * @see appendGroup
	 * @return An array of all of the atoms contained the molecule.
	 * */
	public Atom[] getAtoms()
	{
		return atoms.toArray(new Atom[0]).clone();
	}

	/**
	 * Appends <code>atom0</code> to <code>atom1</code> within the molecule, with a bond described
	 * by <code>bonds</code> and <code>ionic</code> exactly as if those arguments were passed to a
	 * constrcutor of <code>net.chemistry.Bond</code>. This is one of the main ways to modify the 
	 * structure of an existing molecule, the other being <code>appendGroup</code>.
	 * <p>
	 * If the molecule does not contain the atom to which the new atom is supposed to be bonded to, a
	 * <code>java.lang.IllegalArgumentException</code> is thrown. If the specified atoms do not have
	 * enough available valence electrons to bond in the manner specified, a 
	 * <code>net.chemistry.ChemistryException</code> is thrown.
	 * <p>
	 * If the molecule's name has not been modified
	 * by a call to <code>setName</code>, its name is modifed to reflect the presence of the new atom.
	 * 
	 * @throws ChemistryException If the provided atoms cannot bond with each other.
	 * @throws IllegalArgumentException If the provided atoms do not exist.
	 * 
	 * @see appendGroup
	 * */
	public void appendAtom(Atom atom0, Atom atom1, int bonds, boolean ionic) throws ChemistryException
	{
		//System.out.println("death in the molecule");
		
		if (! atoms.contains(atom0))
		{
			throw new IllegalArgumentException("Atoms are not a part of the molecules to be bonded.");
		}
		
		if (atom0.getEInfo().toNextNoble() < bonds)
		{
			throw new ChemistryException("No room to bond " + atom0.toString() + " and " + atom1.toString() + ", too many bonds on " + atom0.toString());
		}
		if (atom1.getEInfo().toNextNoble() < bonds)
		{
			throw new ChemistryException("No room to bond " + atom0.toString() + " and " + atom1.toString() + ", too many bonds on " + atom1.toString());
		}
		
		//System.out.println("got though exception checking");
	
		Bond bond = new Bond(atom0, atom1, bonds, ionic);
		
		atom0.bond(bond);
		atom1.bond(bond);
		
		if (atoms.add(atom1))
		{
			if (atomCount.containsKey(atom1.atomid))
			{
				atomCount.put(atom1.atomid, atomCount.get(atom1.atomid).intValue() + 1);
				
				if (! nameset)
				{
					resetName();
				}
			}
			else
			{
				atomCount.put(atom1.atomid, 1);
			}
		}
		
		//System.out.println("end of death in the molecule");
	}
	
	/**
	 * Appends <code>molecule</code> to the Molecule by joining <code>atom1</code> from that molecule 
	 * to <code>atom0</code> within this molecule with a bond describedby <code>bonds</code> and 
	 * <code>ionic</code> exactly as if those arguments were passed to a constrcutor of 
	 * <code>net.chemistry.Bond</code>. This is one of the main ways to modify the structure of an 
	 * existing molecule, the other being <code>appendAtom</code>.
	 * <p>
	 * If either of the molecules do not contain the atom specified for bonding, or if the provided 
	 * molecule is not valid (e.g. it has already been added to another molecule and no longer
	 * describes a distict entity), a <code>java.lang.IllegalArgumentException</code> is thrown. 
	 * If the specified atoms do not have enough available valence electrons to bond in the manner 
	 * specified, a <code>net.chemistry.ChemistryException</code> is thrown.  
	 * <p>
	 * If the molecule's name has not been modified
	 * by a call to <code>setName</code>, its name is modifed to reflect the presence of the atoms
	 * gained from the new molecule.
	 * 
	 * @throws ChemistryException If the provided atoms cannot bond with each other.
	 * @throws IllegalArgumentException If the provided atoms do not exist, or if the provided molecule
	 * id not valid.
	 * 
	 * @see appendAtom
	 * @see isValid
	 * */
	public void appendGroup(Molecule molecule, Atom atom0, Atom atom1, int bonds, boolean ionic) 
			throws ChemistryException
	{
		if (! atoms.contains(atom0) || ! molecule.atoms.contains(atom1))
		{
			throw new IllegalArgumentException("Atoms are not a part of the molecules to be bonded.");
		}
		
		if (! molecule.valid)
		{
			throw new IllegalArgumentException("Molecule does not exist.");
		}
		
		if (atom0.getEInfo().toNextNoble() < bonds || atom1.getEInfo().toNextNoble() < bonds)
		{
			throw new ChemistryException("No room to bond " + atom0.toString() + " and " 
					+ atom1.toString());
		}
		
		atoms.addAll(molecule.atoms);
		
		for(AtomID atomid : molecule.atomCount.keySet())
		{
			if (atomCount.containsKey(atomid))
			{
				atomCount.put(atomid, atomCount.get(atomid) + molecule.atomCount.get(atomid));
			}
			else
			{
				atomCount.put(atomid, molecule.atomCount.get(atomid));
			}
		}
		
		molecule.valid = false;
	}
	
	/**
	 * Returns the name of the molecule. If no calls to <code>setName</code> have been made at the 
	 * time of this call, or a call has been made to <code>resetName</code> since the last call to 
	 * <code>setName</code>, it will be the formula of the molecule; otherwise it will be whatever 
	 * has been specified in the most recent call to <code>setName</code>.
	 * 
	 * @return The name of the molecule.
	 * @see setName
	 * @see resetName
	 * */
	public String getName()
	{
		return name;
	}
	
	/**
	 * Renders the molecule to the screen via LWJGL and OpenGL. Any transformations must be applied
	 * outside this call.
	 * */
	public void render(int x, int y, int angle)
	{
		root.render(x,y,angle);
		
		for(Atom atom : atoms)
		{
			if (atom.renderInfo())
			{
				break;
			}
			
		}
		
		for(Atom atom : atoms)
		{
			atom.resetDraw();
		}
	}
}
